/*
* Copyright 2012-2016 Luca Zanconato
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.nharyes.drivecopy.srvc;
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.googleapis.json.GoogleJsonResponseException;
import com.google.api.client.googleapis.media.MediaHttpDownloader;
import com.google.api.client.googleapis.media.MediaHttpDownloaderProgressListener;
import com.google.api.client.googleapis.media.MediaHttpUploader;
import com.google.api.client.googleapis.media.MediaHttpUploaderProgressListener;
import com.google.api.client.http.*;
import com.google.api.client.json.JsonFactory;
import com.google.api.services.drive.Drive;
import com.google.api.services.drive.Drive.Files;
import com.google.api.services.drive.Drive.Files.Get;
import com.google.api.services.drive.Drive.Files.Insert;
import com.google.api.services.drive.Drive.Files.Update;
import com.google.api.services.drive.DriveRequest;
import com.google.api.services.drive.model.File;
import com.google.api.services.drive.model.FileList;
import com.google.api.services.drive.model.ParentReference;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import net.nharyes.drivecopy.biz.bo.EntryBO;
import net.nharyes.drivecopy.biz.bo.TokenBO;
import net.nharyes.drivecopy.srvc.exc.FolderNotFoundException;
import net.nharyes.drivecopy.srvc.exc.ItemNotFoundException;
import net.nharyes.drivecopy.srvc.exc.SdoException;
import javax.annotation.Nonnull;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;
@Singleton
public class DriveSdoImpl implements DriveSdo {
/*
* Logger
*/
protected final Logger logger = Logger.getLogger(getClass().getName());
/*
* HTTP Request read and connect timeout
*/
protected static final int HTTP_REQUEST_TIMEOUT = 3 * 600000;
// HTTP transport
protected HttpTransport httpTransport;
// JSON factory
protected JsonFactory jsonFactory;
// File upload progress listener
protected MediaHttpUploaderProgressListener fileUploadProgressListener;
// File download progress listener
protected MediaHttpDownloaderProgressListener fileDownloadProgressListener;
@Inject
public DriveSdoImpl(HttpTransport httpTransport, JsonFactory jsonFactory, MediaHttpUploaderProgressListener fileUploadProgressListener, MediaHttpDownloaderProgressListener fileDownloadProgressListener) {
this.httpTransport = httpTransport;
this.jsonFactory = jsonFactory;
this.fileUploadProgressListener = fileUploadProgressListener;
this.fileDownloadProgressListener = fileDownloadProgressListener;
}
protected Drive getService(@Nonnull TokenBO token) {
final GoogleCredential credential = new GoogleCredential.Builder().setClientSecrets(token.getClientId(), token.getClientSecret()).setJsonFactory(jsonFactory).setTransport(httpTransport).build().setRefreshToken(token.getRefreshToken()).setAccessToken(token.getAccessToken());
return new Drive.Builder(httpTransport, jsonFactory, new HttpRequestInitializer() {
public void initialize(HttpRequest httpRequest) {
try {
// initialize credentials
credential.initialize(httpRequest);
// set connect and read timeouts
httpRequest.setConnectTimeout(HTTP_REQUEST_TIMEOUT);
httpRequest.setReadTimeout(HTTP_REQUEST_TIMEOUT);
} catch (IOException ex) {
// log exception
logger.log(Level.SEVERE, ex.getMessage(), ex);
}
}
}).setApplicationName("DriveCopy").build();
}
protected <T> T executeWithExponentialBackoff(DriveRequest<T> req) throws IOException, InterruptedException {
Random randomGenerator = new Random();
for (int n = 0; n < 5; ++n) {
try {
return req.execute();
} catch (GoogleJsonResponseException e) {
if (e.getStatusCode() == 503 || e.getStatusCode() == 500 || (e.getStatusCode() == 403 && (e.getDetails().getErrors().get(0).getReason().equals("rateLimitExceeded") || e.getDetails().getErrors().get(0).getReason().equals("userRateLimitExceeded")))) {
// apply exponential backoff.
Thread.sleep((1 << n) * 1000 + randomGenerator.nextInt(1001));
} else {
// other error, re-throw.
throw e;
}
}
}
throw new IOException("There has been an error, the request never succeeded.");
}
public EntryBO downloadEntry(@Nonnull TokenBO token, @Nonnull EntryBO entry) throws SdoException {
try {
// get file
Drive service = getService(token);
Get get = service.files().get(entry.getId());
MediaHttpDownloader downloader = new MediaHttpDownloader(httpTransport, service.getRequestFactory().getInitializer());
downloader.setDirectDownloadEnabled(false);
downloader.setProgressListener(fileDownloadProgressListener);
File file = executeWithExponentialBackoff(get);
// check download URL and size
if (file.getDownloadUrl() != null && file.getDownloadUrl().length() > 0) {
// download file
FileOutputStream fout = new FileOutputStream(entry.getFile());
downloader.download(new GenericUrl(file.getDownloadUrl()), fout);
fout.flush();
fout.close();
// return entry
entry.setMd5Sum(file.getMd5Checksum());
return entry;
} else {
// the file doesn't have any content stored on Drive
throw new ItemNotFoundException(String.format("Remote file with id '%s' doesn't have any content stored on Drive", entry.getId()));
}
} catch (IOException | InterruptedException ex) {
// re-throw exception
throw new SdoException(ex.getMessage(), ex);
}
}
public EntryBO uploadEntry(@Nonnull TokenBO token, @Nonnull EntryBO entry, @Nonnull String parentId) throws SdoException {
try {
// create file item
File body = new File();
body.setTitle(entry.getName());
body.setMimeType(entry.getMimeType());
// set parent
ParentReference newParent = new ParentReference();
newParent.setId(parentId);
body.setParents(new ArrayList<ParentReference>());
body.getParents().add(newParent);
// set content
FileContent mediaContent = new FileContent(entry.getMimeType(), entry.getFile());
// upload file
Insert insert = getService(token).files().insert(body, mediaContent);
MediaHttpUploader uploader = insert.getMediaHttpUploader();
uploader.setDirectUploadEnabled(false);
uploader.setProgressListener(fileUploadProgressListener);
File file = executeWithExponentialBackoff(insert);
// compose output entry
EntryBO entryBO = new EntryBO();
entryBO.setId(file.getId());
entryBO.setFile(entry.getFile());
entryBO.setMd5Sum(file.getMd5Checksum());
return entryBO;
} catch (IOException | InterruptedException ex) {
// re-throw exception
throw new SdoException(ex.getMessage(), ex);
}
}
public EntryBO updateEntry(@Nonnull TokenBO token, @Nonnull EntryBO entry) throws SdoException {
try {
// get file
Drive service = getService(token);
File file = executeWithExponentialBackoff(service.files().get(entry.getId()));
// update file content
FileContent mediaContent = new FileContent(entry.getMimeType(), entry.getFile());
file.setMimeType(entry.getMimeType());
// update file
Update update = service.files().update(entry.getId(), file, mediaContent);
update.setNewRevision(!entry.isSkipRevision());
MediaHttpUploader uploader = update.getMediaHttpUploader();
uploader.setDirectUploadEnabled(false);
uploader.setProgressListener(fileUploadProgressListener);
File updatedFile = executeWithExponentialBackoff(update);
// compose output entry
EntryBO docBO = new EntryBO();
docBO.setId(updatedFile.getId());
docBO.setName(updatedFile.getTitle());
docBO.setFile(entry.getFile());
docBO.setMd5Sum(updatedFile.getMd5Checksum());
return docBO;
} catch (IOException | InterruptedException ex) {
// re-throw exception
throw new SdoException(ex.getMessage(), ex);
}
}
public String getLastFolderId(@Nonnull TokenBO token, String[] folders, @Nonnull String rootId, boolean createIfNotFound) throws SdoException {
Drive service = getService(token);
try {
// check folders
String lastParentId = rootId;
String lastParentName = null;
if (folders != null) {
// check folders existence
for (String currentFolder : folders) {
try {
// compose current folder query
Files.List request = service.files().list();
request.setQ(String.format("title = '%s' and trashed = false and mimeType = 'application/vnd.google-apps.folder' and '%s' in parents", currentFolder, lastParentId));
request.setMaxResults(2);
// execute query
logger.finer(String.format("Search remote folder with name '%s'", currentFolder));
FileList fs = executeWithExponentialBackoff(request);
// check no results
if (fs.getItems().isEmpty())
throw new FolderNotFoundException(String.format("No remote folder found with name '%s'%s", currentFolder, lastParentName != null ? String.format(" in remote folder '%s'", lastParentName) : ""));
// check multiple results
if (fs.getItems().size() > 1)
throw new SdoException(String.format("Multiple results for remote folder with name '%s'%s", currentFolder, lastParentName != null ? String.format(" in remote folder '%s'", lastParentName) : ""));
// check exact title
File folder = fs.getItems().get(0);
if (!folder.getTitle().equals(currentFolder))
throw new FolderNotFoundException(String.format("No remote folder found with exact name '%s'%s", currentFolder, lastParentName != null ? String.format(" in remote folder '%s'", lastParentName) : ""));
// set parent ID for next folder/file
lastParentId = folder.getId();
lastParentName = folder.getTitle();
} catch (FolderNotFoundException ex) {
// in case re-throw exception
if (!createIfNotFound)
throw ex;
// create folder
logger.finer(String.format("Create remote folder with name '%s'", currentFolder));
File folder = new File();
folder.setTitle(currentFolder);
folder.setMimeType("application/vnd.google-apps.folder");
folder.setParents(Collections.singletonList(new ParentReference().setId(lastParentId != null ? lastParentId : "root")));
folder = executeWithExponentialBackoff(service.files().insert(folder));
// set parent ID for next folder/file
lastParentId = folder.getId();
lastParentName = folder.getTitle();
}
}
}
return lastParentId;
} catch (IOException | InterruptedException ex) {
// re-throw exception
throw new SdoException(ex.getMessage(), ex);
}
}
public EntryBO searchEntry(@Nonnull TokenBO token, @Nonnull String name, @Nonnull String parentId) throws SdoException {
try {
// compose list query
Files.List request = getService(token).files().list();
request.setQ(String.format("title = '%s' and trashed = false and mimeType != 'application/vnd.google-apps.folder' and '%s' in parents", name, parentId));
request.setMaxResults(2);
// execute query
logger.finer(String.format("Search entry with name '%s'", name));
FileList files = executeWithExponentialBackoff(request);
// check no results
if (files.getItems().isEmpty())
throw new ItemNotFoundException(String.format("No remote file found with name '%s'", name));
// check multiple results
if (files.getItems().size() > 1)
throw new SdoException(String.format("Multiple results for entry with name '%s'", name));
// check exact title
File file = files.getItems().get(0);
if (!file.getTitle().equals(name))
throw new ItemNotFoundException(String.format("No remote file found with exact name '%s'", name));
// return entry
EntryBO entry = new EntryBO();
entry.setId(file.getId());
entry.setName(file.getTitle());
entry.setMd5Sum(file.getMd5Checksum());
entry.setMimeType(file.getMimeType());
return entry;
} catch (IOException | InterruptedException ex) {
// re-throw exception
throw new SdoException(ex.getMessage(), ex);
}
}
}